﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Xml;
using System.Linq;
using System.Text;
using PServiceBus.Core.Provider;
using Phoenix.ReflectionExtensions;
using PServiceBus.Core.Runtime.Extensions;
using System.Collections;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using PServiceBus.Core.Runtime.Transports;

namespace PServiceBus.Core.Runtime {
    public static class ReflectionHelper {

        private static readonly Dictionary<Type, Type> CachedType = new Dictionary<Type, Type>();
        private static readonly Dictionary<string, Type> CachedDictClassType = new Dictionary<string, Type>();
        private static readonly Dictionary<Type, Func<object>> CachedDictFuncNew = new Dictionary<Type, Func<object>>();
        private const MethodAttributes ConstructorAttribute = MethodAttributes.Public | MethodAttributes.HideBySig;

        private const BindingFlags PropertyBinding = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public;

        public const MethodAttributes MethodAttribute =
            MethodAttributes.Public
            | MethodAttributes.Virtual
            | MethodAttributes.Final
            | MethodAttributes.HideBySig
            | MethodAttributes.NewSlot
            | MethodAttributes.SpecialName;

        
        private static readonly ConstructorInfo NotImplementCtor = typeof(NotImplementedException).GetConstructor(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,  null, new Type[]{}, null);

        private static readonly object _dictFuncLockObject = new object();
        private static readonly ConstructorInfo DataContractCtor = typeof(DataContractAttribute).GetConstructor(Type.EmptyTypes);
        private static readonly ConstructorInfo DataMemberCtor = typeof(DataMemberAttribute).GetConstructor(Type.EmptyTypes);
        private static readonly PropertyInfo DataContractNamespaceProp = typeof(DataContractAttribute).GetPropertyEx("Namespace");
        private static readonly PropertyInfo DataContractNameProp = typeof(DataContractAttribute).GetPropertyEx("Name");

        private static Func<object> ILNew<T>() where T : class {
            return ILNew(typeof(T));
        }

        private static Func<object> ExpressionNew<T>() where T : class {
            return ExpressionNew(typeof(T));
        }

        private static Func<object> ExpressionNew(Type type) {
            lock (_dictFuncLockObject) {
                if (CachedDictFuncNew.ContainsKey(type)) return CachedDictFuncNew[type];
                var generatedType = GenerateInterface(type);
                var method = generatedType.IsValueType ? () => Activator.CreateInstance(generatedType) :
                    generatedType == typeof(String) ? () => string.Empty as object :
                    Expression.Lambda<Func<object>>(Expression.New(generatedType.GetConstructor(Type.EmptyTypes))).Compile();
                CachedDictFuncNew[type] = method;
                return method;
            }
        }

        private static Func<object> ILNew(Type type) {
            if(CachedDictFuncNew.ContainsKey(type)) return CachedDictFuncNew[type];
            var generatedType = GenerateInterface(type);
            var dynMethod = new DynamicMethod("dynamicMethod" + generatedType.FullName, typeof(object), null, type);
            var ilGen = dynMethod.GetILGenerator();

            ilGen.Emit(OpCodes.Newobj, generatedType.GetConstructor(Type.EmptyTypes));
            ilGen.Emit(OpCodes.Ret);
            var method = (Func<object>)dynMethod.CreateDelegate(typeof(Func<object>));
            CachedDictFuncNew[type] = method;
            return method;
        }

        /// <summary>
        /// Generate a proxy class for specific type. 
        /// If type is an interface, It generate a class that implement the interface
        /// </summary>
        /// <typeparam name="TClass"></typeparam>
        /// <param name="action">Action to set the newly generated type properties</param>
        /// <returns></returns>
        public static TClass New<TClass>(Action<TClass> action) where TClass : class {
            var newAction = ExpressionNew(GenerateInterface<TClass>());
            var obj = newAction() as TClass;//Activator.CreateInstance(GenerateInterface<TClass>()) as TClass;
            action(obj);
            return obj;
        }

        /// <summary>
        /// Initialize type using IL/ExpressionTree
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static object New(Type type) {
            return ExpressionNew(GenerateInterface(type))();
        }

        /// <summary>
        ///  Generate a proxy class for specific type. 
        /// If type is an interface, It generate a class that implement the interface
        /// </summary>
        /// <typeparam name="TClass"></typeparam>
        /// <returns></returns>
        public static TClass New<TClass>() where TClass : class {
            var newAction = ExpressionNew(GenerateInterface<TClass>());
            var obj = newAction() as TClass;//Activator.CreateInstance(GenerateInterface<TClass>()) as TClass;
            return obj;
        }


        /// <summary>
        /// Generate a class with properties based on the specified dictionary
        /// </summary>
        /// <param name="dict"></param>
        /// <returns></returns>
        public static object GenerateClass(string name, Dictionary<string, object> dict) {
            var type = GenerateClassType(name, dict);
            var obj = Activator.CreateInstance(type);
            var props = type.GetPropertiesEx();
            foreach (var prop in props) {
                var propType = prop.PropertyType;
                var value = dict[prop.Name].ObjectToJson();
                var isArray = propType.IsArray;
                var isPrimitive = prop.PropertyType.GetConstructor(Type.EmptyTypes) == null;
                if (!isPrimitive) {
                    var isValueDict = value.GetType() == _dictType;
                    var valueDict = isValueDict ? value as Dictionary<string, object> : null;
                    var isRealDict = isValueDict && valueDict.ContainsKey(_typeIdentifier) && valueDict[_typeIdentifier].ToString() == "dict";
                    var isClass = isValueDict && !isRealDict;
                    if (isClass) {
                        valueDict.Remove(_typeIdentifier);
                        value = GenerateClass(name + prop.Name, valueDict);
                    } else {
                        var dictObj = Activator.CreateInstance(propType) as IDictionary;
                        foreach (var kv in valueDict)
                            dictObj[kv.Key] = kv.Value;
                        value = dictObj;
                    }
                }
                if (isArray) {
                    var array = value as object[];
                    var elemType = propType.GetElementType();
                    var elemArray = Array.CreateInstance(elemType, array.Length);
                    Array.Copy(array, elemArray, array.Length);
                    value = elemArray;
                }
                prop.SetValueEx(obj, value);
            }
            return obj;
        }

        private static readonly Type _dictType = typeof(Dictionary<string, object>);
        private static readonly Type _dictKVType = typeof(Dictionary<,>);
        private static readonly Type _idictType = typeof(IDictionary);
        private static readonly Type _objectType = typeof(object);
        private static readonly string _typeIdentifier = String.Intern("___type");
        private static readonly string _typeNameIdentifier = String.Intern("___typename");
        private static readonly string _typeNamespaceIdentifier = String.Intern("___typenamespace");
        private static readonly string[] _typeKeys = new[] { _typeIdentifier, _typeNameIdentifier, _typeNamespaceIdentifier };
        private static readonly string _defaultTypeNamespace = "http://tempuri.org/";

        /// <summary>
        /// Generate type based on dictionary and use ___type == 'dict' to determine if the object is really a dict or just another class
        /// </summary>
        /// <param name="name"></param>
        /// <param name="dict"></param>
        /// <returns></returns>
        public static Type GenerateClassType(string name, Dictionary<string, object> dict) {
            var hasTypeNameInDict = dict.ContainsKey(_typeNameIdentifier);
            name = (name ?? string.Empty).ToLower();
            name = hasTypeNameInDict ? dict[_typeNameIdentifier].ToString() : name;
            if (CachedDictClassType.ContainsKey(name)) return CachedDictClassType[name];
            
            var hasTypeNamespaceInDict = dict.ContainsKey(_typeNamespaceIdentifier);
            var typeNamespace = hasTypeNamespaceInDict ? dict[_typeNamespaceIdentifier].ToString() : _defaultTypeNamespace;
            if (hasTypeNameInDict) dict.Remove(_typeNameIdentifier);
            if (hasTypeNamespaceInDict) dict.Remove(_typeNamespaceIdentifier);
            
            var className = name.Replace(".", string.Empty);
            var newTypeName = hasTypeNameInDict ? name :  (className + "ClassDTO");
            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(newTypeName) { Version = new Version(1, 0, 0, 0) },
                                                                           AssemblyBuilderAccess.RunAndSave);
            var module = assembly.DefineDynamicModule(newTypeName + ".dll");
            var type = module.DefineType(newTypeName, TypeAttributes.Public | TypeAttributes.Serializable, typeof(Object));
            type.SetCustomAttribute(new CustomAttributeBuilder(DataContractCtor, new object[] { },
                new PropertyInfo[] { DataContractNameProp, DataContractNamespaceProp }, new object[] { name, typeNamespace}));
            
            foreach (var prop in dict) {
                var propValue = prop.Value;
                var pType = propValue.GetType();
                var propName = prop.Key;
                var propClassName = name + propName;
                pType = pType == typeof(string) && propValue.ToString().IsJsonDate() ? typeof(DateTime) : pType;
                var isValueDict = pType == _dictType || _idictType.IsAssignableFrom(pType);
                var valueDict = isValueDict ? propValue as Dictionary<string, object> : null;
                var isRealDict = isValueDict && valueDict.ContainsKey(_typeIdentifier) && valueDict[_typeIdentifier].ToString() == "dict";
                var isClass = isValueDict && !isRealDict;
                if (isValueDict) valueDict.Remove(_typeIdentifier);
                var dictKeyValue = isRealDict ?
                    valueDict.Where(kv =>
                        (!_typeKeys.Contains(kv.Key) && kv.Value.GetType() == _dictType) || !_typeKeys.Contains(kv.Key))
                    .FirstOrDefault() as KeyValuePair<string, object>? : null;
                var dictKeyValueDict = dictKeyValue != null ? 
                    dictKeyValue.Value.Value as Dictionary<string, object> : null;
                var dictRealValueType = isRealDict && dictKeyValueDict == null ? dictKeyValue.Value.Value.GetType() : null;
                var dictValueType = dictKeyValueDict != null ?
                    GenerateClassType(propClassName, dictKeyValueDict) : dictRealValueType;
                pType = dictValueType != null ?
                    typeof(Dictionary<,>).MakeGenericType(typeof(string), dictValueType.IsValueType ? typeof(string) : dictValueType)
                    : pType;
                var propType = isClass ? GenerateClassType(propClassName, valueDict) : pType;
                var pList = propValue as IList;
                if (pList != null) {
                    var pItem = typeof(IList).GetPropertyEx("Item");
                    var pValue = pItem.GetValue(propValue, new object[] { 0 });
                    var pValueDict = pValue as Dictionary<string, object>;
                    if (pValueDict != null) propType = GenerateClassType(propClassName, pValueDict).MakeArrayType();
                    else propType = pValue.GetType().MakeArrayType();
                }
                if (isRealDict) valueDict[_typeIdentifier] = "dict";
                //Create  Property
                var getSetProp = type.DefineProperty(propName, PropertyAttributes.None,
                                                     propType, Type.EmptyTypes);

                
                //Get Set Field
                var setGetField = type.DefineField("_" + propName.ToLower(), propType, FieldAttributes.Private);

                //Set Method

                var setMethod = type.DefineMethod("set_" + propName, MethodAttribute);
                setMethod.SetReturnType(typeof(void));
                setMethod.SetParameters(propType);
                var setGen = setMethod.GetILGenerator();

                //Write Set Body
                setGen.Emit(OpCodes.Ldarg_0);
                setGen.Emit(OpCodes.Ldarg_1);
                setGen.Emit(OpCodes.Stfld, setGetField);
                setGen.Emit(OpCodes.Ret);

                //Get Method
                var getMethod = type.DefineMethod("get_" + propName, MethodAttribute);
                getMethod.SetReturnType(propType);
                var getGen = getMethod.GetILGenerator();

                //Write Get Body
                getGen.Emit(OpCodes.Ldarg_0);
                getGen.Emit(OpCodes.Ldfld, setGetField);
                getGen.Emit(OpCodes.Ret);

                //Attach Get/Set Method to Get/Set Property
                getSetProp.SetGetMethod(getMethod);
                getSetProp.SetSetMethod(setMethod);

                getSetProp.SetCustomAttribute(new CustomAttributeBuilder(DataMemberCtor, new object[] { }));
            }
            //Restore type name/namespace if it exists
            if (hasTypeNameInDict) dict[_typeNameIdentifier] = name;
            if (hasTypeNamespaceInDict) dict[_typeNamespaceIdentifier] = typeNamespace;
            var returnType = type.CreateType();
            CachedDictClassType[name] = returnType;
            return returnType;
        }

        public static Type GenerateInterface(Type interfaceType) {
            if (interfaceType.IsPrimitive) return interfaceType;
            if (!interfaceType.IsAbstract && interfaceType.IsClass) return interfaceType;
            if (CachedType.ContainsKey(interfaceType)) return CachedType[interfaceType];
            var newTypeName = interfaceType.Name.Substring(1) + "ClassDTO";
            var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(newTypeName) { Version = new Version(1, 0, 0, 0) },
                                                                           AssemblyBuilderAccess.RunAndSave);
            var module = assembly.DefineDynamicModule(newTypeName + ".dll");
            var type = module.DefineType(newTypeName, TypeAttributes.Public | TypeAttributes.Serializable, typeof(Object), new[] { interfaceType });

            var props = interfaceType.GetProperties(PropertyBinding).Union(interfaceType.GetInterfaces().SelectMany(it => it.GetProperties(PropertyBinding)));

            foreach (var prop in props) {
                var prefix = prop.ReflectedType.FullName + ".";
                var customAttributes = prop.GetCustomAttributes(true);
                //Create Interface Property
                var getSetProp = type.DefineProperty(prop.Name, PropertyAttributes.HasDefault,
                                                     prop.PropertyType, Type.EmptyTypes);


                foreach (var attribute in customAttributes) {
                    var attributeCtor = attribute.GetType().GetConstructor(Type.EmptyTypes);
                    getSetProp.SetCustomAttribute(new CustomAttributeBuilder(attributeCtor, new object[] { }));
                }

                var getSetGetMethod = prop.GetGetMethod();
                var getSetSetMethod = prop.GetSetMethod();

                //Get Set Field
                var setGetField = type.DefineField("_" + prop.Name.ToLower(), prop.PropertyType, FieldAttributes.Private);

                //Set Method

                var setMethod = type.DefineMethod(prefix + getSetSetMethod.Name, MethodAttribute);
                setMethod.SetReturnType(typeof(void));
                setMethod.SetParameters(prop.PropertyType);
                var setGen = setMethod.GetILGenerator();

                //Write Set Body
                setGen.Emit(OpCodes.Ldarg_0);
                setGen.Emit(OpCodes.Ldarg_1);
                setGen.Emit(OpCodes.Stfld, setGetField);
                setGen.Emit(OpCodes.Ret);

                //Get Method
                var getMethod = type.DefineMethod(prefix + getSetGetMethod.Name, MethodAttribute);
                getMethod.SetReturnType(prop.PropertyType);
                var getGen = getMethod.GetILGenerator();

                //Write Get Body
                getGen.Emit(OpCodes.Ldarg_0);
                getGen.Emit(OpCodes.Ldfld, setGetField);
                getGen.Emit(OpCodes.Ret);

                //Attach Get/Set Method to Get/Set Property
                getSetProp.SetGetMethod(getMethod);
                getSetProp.SetSetMethod(setMethod);

                //Implement Interface Property
                type.DefineMethodOverride(getMethod, getSetGetMethod);
                type.DefineMethodOverride(setMethod, getSetSetMethod);
            }

            var methods = interfaceType.GetMethods().Union(interfaceType.GetInterfaces().SelectMany(it => it.GetMethods())); ;

            //Define Methods
            foreach (var method in methods) {
                if (method.Name.StartsWith("set_") || method.Name.StartsWith("get_")) continue;
                var methodName = method.ReflectedType.FullName + "." + method.Name;
                var methodBuilder = type.DefineMethod(methodName, MethodAttribute, method.ReturnType,
                    method.GetParameters().Select(p => p.ParameterType).ToArray());

                var methodIL = methodBuilder.GetILGenerator();
                methodIL.Emit(OpCodes.Nop);
                methodIL.Emit(OpCodes.Newobj, NotImplementCtor);
                methodIL.Emit(OpCodes.Throw);

                //Implement Interface Method
                type.DefineMethodOverride(methodBuilder, method);
            }

            var returnType = type.CreateType();
            CachedType[interfaceType] = returnType;
            return returnType;
        }

        /// <summary>
        /// Generate a type based on the specific type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static Type GenerateInterface<T>() where T : class {
            return GenerateInterface(typeof(T));
        }
    }
}
